--
-- Author:zhangxuri
-- Date:2021-04-14
--


local kong = kong
local ngx = ngx
local string = string
local time = ngx.time
local json = require "cjson"
local kong_shared = ngx.shared.kong

local singletons = require "kong.singletons"
local constants = require "kong.constants"
local uuid = require "kong.tools.utils".uuid
local plugin_name = ({...})[1]:match("^kong%.plugins%.([^%.]+)")
-- local http_sender = require "kong.plugins.plugin-manage.sender.http_sender"
local http_sender = require("kong.plugins." .. plugin_name .. ".sender.http_sender")
-- local tools = require "kong.plugins.plugin-manage.tools"
local tools = require("kong.plugins." .. plugin_name .. ".tools")


local HTTP_BAD_REQUEST = 400
local HTTP_OK = 200

local INSERT_POD_INFO_SQL = [[
INSERT INTO cluster_pods (
  `id`,
  `pod_start_time`,
  `pod_last_seen`,
  `pod_last_reload`,
  `pod_hostname`,
  `pod_ip`,
  `pod_plugins`,
  `cluster_id`,
  `pod_cluster`,
  `pod_config_version`)
VALUES (
  '%s',    -- pod_id
  now(),   -- pod_start_time
  now(),   -- pod_last_seen
  now(),   -- pod_last_reload
  '%s',    -- pod_hostname
  '%s',    -- pod_ip
  '%s',    -- pod_plugins
  '%s',    -- cluster_id
  '%s',    -- pod_cluster
  '%s')    -- pod_config_version
ON DUPLICATE KEY UPDATE
  `pod_last_reload` = now(),
  `pod_last_seen` = now(),
  `pod_ip` = '%s',
  `pod_plugins` = '%s',
  `cluster_id` = '%s',
  `pod_cluster` = '%s',
  `pod_config_version` = '%s';
]]

local UPDATE_POD_INFO_SQL = [[
UPDATE cluster_pods
SET `%s` = %s
WHERE `pod_hostname` = '%s';
]]

local SELECT_MASTER_POD_ALIVE_SQL = [[
SELECT TIMESTAMPDIFF(SECOND, pod_last_seen, current_timestamp()) as interval_last_seen
FROM cluster_pods
WHERE `pod_hostname` = '%s';
]]

local KONG_RELOAD_CMD = [[
echo "/usr/local/bin/kong reload --vv -c" %s
/usr/local/bin/kong reload --vv -c %s
]]


-- local KONG_RELOAD_CMD = [[
-- kong /opt/kong/kong/bin/reload --vv -c %s
-- ]]


--- 校验插件是否被使用
-- @param plugin_name 插件名
local function check_plugin_in_use(plugin_name)
  local plugin_name = string.lower(plugin_name)

  local action_position = {}

  for row, err in kong.db.plugins:each() do
    if err then
      kong.log.err(err)
      return nil, err
    end
    if(string.lower(row.name) == plugin_name) then
      local type_and_id

      if row.consumer then
        type_and_id = "consumer:" .. row.consumer.id

      elseif row.service then
        type_and_id = "service:" .. row.service.id

      elseif row.route then
        type_and_id = "route:" .. row.route.id

      else
        type_and_id = "global"
      end

      table.insert(action_position, type_and_id)
    end
  end

  return action_position
end


--- 回滚插件文件
-- @param plugin_exist_tag 插件文件存在标记
-- @param plugins_path 插件路径
-- @param plugin_name 插件名
-- @param new_plugin_name 暂存插件名
local function roll_back_move_plugin(plugin_exist_tag, plugins_path, plugin_name, new_plugin_name)

  if not plugin_exist_tag then
    return true, ""
  end

  local ok, err  = os.execute("mv " .. plugins_path .. new_plugin_name .. " " .. plugins_path .. plugin_name)
  if not ok then
    return false, "更新回滚失败：" .. err .. "\n"
  end

  return true, "更新回滚成功。\n"
end


--- 获取已加载插件列表
local function get_loaded_plugins_list()
  local plugins_loaded = singletons.configuration.loaded_plugins
  return plugins_loaded
end


--- 获取已使用插件列表
local function get_in_use_plugins_list()
  local plugins_in_use = {}

  for row, err in kong.db.plugins:each() do
    if err then
      kong.log.err(err)
      return nil, err
    end

    plugins_in_use[row.name] = true
  end
  return plugins_in_use
end


--- 获取已存在插件列表
local function file_existed_plugins_list(plugins_path)
  local plugins_path = plugins_path or tools.get_default_plugins_path()
  local plugins_file_existed = {}

  if tools.file_exists(plugins_path) then
    -- 使用ls命令列出插件文件夹内容
    local exe = io.popen("ls ".. plugins_path .."| awk '{print $0}'")

    for row in exe:lines() do
      plugins_file_existed[row] = true
    end

    exe:close()
  end
  return plugins_file_existed
end


--- 分割插件列表字符串
-- @param str 待分割字符串
-- @param split_char 分割符
local function split(str, split_char)
  if not tools.is_present(str) then
    return nil, "待分割字符串为空。"
  end

  if not tools.is_present(split_char) then
    return nil, "分割符为空。"
  end

  local sub_str_tab = {}

  while true do

    local pos = string.find(str, split_char)

    if not pos then
      sub_str_tab[str] = "custom"
      break
    end

    local sub_str = string.sub(str, 1, pos - 1)
    sub_str_tab[sub_str] = "custom"

    str = string.sub(str, pos + 1, string.len(str))
  end
  return sub_str_tab
end


--- 分割lua_path字符串
-- @param str 待分割字符串
-- @param split_char 分割符
local function lua_path_split(str, split_char)
  local str = str

  if not tools.is_present(str) then
    return nil, "待分割字符串为空。"
  end

  if not tools.is_present(split_char) then
    return nil, "分割符为空。"
  end

  str = string.gsub(str, " ", "")
  str = string.gsub(str, ".lua", "")
  str = string.gsub(str, "init.lua", "")
  str = string.gsub(str, "?", "kong/plugins")
  str = string.gsub(str, "=", "")

  local sub_str_tab = {}

  while true do

    local pos = string.find(str, split_char)

    if not pos then
      if #str > 0 then
        table.insert(sub_str_tab, str)
      end
      break
    end

    local sub_str = string.sub(str, 1, pos - 1)
    if #sub_str > 0 then
      table.insert(sub_str_tab, sub_str)
    end

    str = string.sub(str, pos + 1, string.len(str))
  end
  return sub_str_tab
end


--- 获取已存在文件的自定义插件列表
local function get_file_existed_plugins_list(conf_path, conf_name)
  local conf_path =conf_path
  local conf_name =conf_name
  local file, err = io.open(conf_path .. conf_name, "r")
  if not file then
    return nil, "配置文件读取失败：" .. err
  end
  local existed_plugins_list = {}
  for line in file:lines() do
    if(string.sub(line, 1, 16) == "lua_package_path") then
      local lua_path
      lua_path = string.sub(line, string.find(line, "="), (string.find(line, "#") or 0) - 1)
      if not lua_path then
        return nil, "配置文件无lua_path信息。"
      end
      local split_res, err = lua_path_split(lua_path ,";")
      if err then
        return nil, "conf文件内lua_path分割失败：" .. err
      end
      for _, v in pairs(split_res) do
        local find_res = file_existed_plugins_list(v)
        if find_res then
          for k, _ in pairs(find_res) do
            existed_plugins_list[k] = true
          end
        end
      end
      break
    end
  end
  file.close()
  return existed_plugins_list
end


--- 获取bundled插件列表
local function get_bundled_plugins_list()
  local bundled_plugins = constants.BUNDLED_PLUGINS
  return bundled_plugins
end


--- 获取conf文件已注册的插件列表
local function get_registered_plugins_list(conf_path, conf_name)
  local conf_path = conf_path
  local conf_name = conf_name

  local file, err = io.open(conf_path .. conf_name, "r")
  if not file then
    local msg = "配置文件读取失败：" .. err
    return nil, msg
  end

  local list

  for line in file:lines() do
    if(string.sub(line, 1, 10) == "plugins = ") then
      list = string.sub(line, 11, (string.find(line, " ", 11) or 0) - 1)
      if not list then
        return nil, "未找到已注册的自定义插件。"
      end
      break
    end
  end
  if not list then
    file:close()
    return nil, "配置文件无自定义插件信息。"
  end

  local registered_plugins, err = split(list, ",")
  if not registered_plugins then
    file:close()
    return nil, err
  end

  local bundled =  get_bundled_plugins_list()
  if registered_plugins["bundled"] then
    registered_plugins["bundled"] = nil
    for k,_ in pairs(bundled) do
      registered_plugins[k] = "bundled"
    end
  end

  file:close()
  return registered_plugins
end


--- 注册插件
local function register_plugin(plugin_name)
  local plugin_name = plugin_name
  local conf_path = tools.get_default_conf_path()
  local conf_name = tools.get_default_conf_name()
  local absolute_path = conf_path .. conf_name

  local file, err = io.open(absolute_path, "r")
  if not file then
    local msg = "配置文件读取失败：" .. err
    return nil, msg
  end

  local list
  local plugins_line
  local lines = {}
  local restOfFile

  -- 循环读配置文件
  for line in file:lines() do
    if(string.sub(line, 1, 10) == "plugins = ") then
      plugins_line = line
      -- 获取插件串
      list = string.sub(plugins_line, 11, (string.find(plugins_line, " ", 11) or 0) - 1)
      -- 存入插件配置后面行
      restOfFile = file:read("*a")
      break
    else
      -- 存入插件配置前面各行
      lines[#lines + 1] = line
    end
  end
  file:close()

  -- 插件列表
  local registered_plugins

  -- 插件串为空或未配置则生成默认插件列表
  if not plugins_line or not list then
    registered_plugins = {}
    registered_plugins["bundled"] = true

  else
    registered_plugins, err = split(list, ",")
    if err then
      registered_plugins = {}
      --todo return
    end
  end

  -- 检查是否为bundled插件
  if registered_plugins["bundled"] then
    local bundled =  get_bundled_plugins_list()

    for k,_ in pairs(bundled) do
      if k == plugin_name then
        local msg = "插件为bundled插件，bundled已注册。\n"
        local _, update_msg = tools.update_config_version()
        msg = msg .. update_msg
        return true, msg
      end
    end
  end

  for k,_ in pairs(registered_plugins) do
    if k == plugin_name then
      local msg = "插件已注册。\n"
      return true, msg
    end
  end

  registered_plugins[plugin_name] = "custom"
  local plugins_line_new   = "plugins = "
  for k, _ in pairs(registered_plugins) do
    plugins_line_new = plugins_line_new .. k .. ","
  end

  -- 去除末尾逗号
  plugins_line_new = string.sub(plugins_line_new, 1, -2)
  lines[#lines + 1] = plugins_line_new

  file, err = io.open(absolute_path, "w")
  if not file then
    local msg = "配置文件写入失败：" .. err .. "\n"
    return nil, msg
  end

  for _, line in pairs(lines) do
    file:write(line, "\n")
  end
  file:write(restOfFile)
  file:close()

  local msg = "插件注册成功。\n"
  local _, update_msg = tools.update_config_version()
  msg = msg .. update_msg
  return true, msg
end


--- 注销插件
local function unregister_plugin(plugin_name)
  local plugin_name = plugin_name
  local conf_path = tools.get_default_conf_path()
  local conf_name = tools.get_default_conf_name()
  local absolute_path = conf_path .. conf_name

  local file, err = io.open(absolute_path, "r")
  if not file then
    local msg = "配置文件读取失败：" .. err .. "\n"
    return nil, msg
  end

  local list
  local plugins_line
  local lines = {}
  local restOfFile

  -- 循环读配置文件
  for line in file:lines() do
    if(string.sub(line, 1, 10) == "plugins = ") then
      plugins_line = line
      -- 获取插件串
      list = string.sub(plugins_line, 11, (string.find(plugins_line, " ", 11) or 0) - 1)
      -- 存入插件配置后面行
      restOfFile = file:read("*a")
      break
    else
      -- 存入插件配置前面各行
      lines[#lines + 1] = line
    end
  end
  file:close()

  -- 插件列表
  local registered_plugins

  -- 插件串为空或未配置则生成默认插件列表
  if not plugins_line or not list then
    local msg = "未配置自定义插件。\n"
    return nil, msg

  else
    registered_plugins, err = split(list, ",")
    if err then
      local msg = "自定义插件列表为空或自定义插件列表解析错误：" .. err .. "\n"
      return nil, msg
    end
  end

  if not registered_plugins[plugin_name] then
    local msg = "插件未注册。\n"
    -- local _, update_msg = tools.update_config_version()
    -- msg = msg .. update_msg
    return true, msg
  end

  registered_plugins[plugin_name] = nil
  local plugins_line_new   = "plugins = "
  for k, _ in pairs(registered_plugins) do
    plugins_line_new = plugins_line_new .. k .. ","
  end

  -- 去除末尾逗号
  plugins_line_new = string.sub(plugins_line_new, 1, -2)
  lines[#lines + 1] = plugins_line_new

  file, err = io.open(absolute_path, "w")
  if not file then
    local msg = "配置文件写入失败：" .. err .. "\n"
    return nil, msg
  end

  for _, line in pairs(lines) do
    file:write(line, "\n")
  end
  file:write(restOfFile)
  file:close()

  local msg = "插件注销成功。\n"
  local _, update_msg = tools.update_config_version()
  msg = msg .. update_msg

  return true, msg
end


--- 插件列表
local function plugin_list(conf_path, conf_name)
  local conf_path = conf_path
  local conf_name = conf_name

  local plugin_list = {}
  -- 自定义插件
  local plugins_file_existed, err = get_file_existed_plugins_list(conf_path, conf_name)
  if err then
    return nil, err
  end

  -- 已使用插件
  local plugins_in_use, err = get_in_use_plugins_list()
  if err then
    return nil, err
  end

  -- 已加载插件
  local plugins_loaded = get_loaded_plugins_list()

  -- 已注册插件
  local plugins_registered, err = get_registered_plugins_list(conf_path, conf_name)
  if not plugins_registered then
    return nil, err
  end

  -- 整合入plugin_list
  for k, v in pairs(plugins_file_existed) do
    if not plugin_list[k] then
      plugin_list[k] = {}
    end
    plugin_list[k]["plugin_file_existed"] = v
  end

  for k, v in pairs(plugins_loaded) do
    if not plugin_list[k] then
      plugin_list[k] = {}
    end
    plugin_list[k]["plugin_loaded"] = v
  end

  for k, v in pairs(plugins_in_use) do
    if not plugin_list[k] then
      plugin_list[k] = {}
    end
    plugin_list[k]["plugin_in_use"] = v
  end

  for k, v in pairs(plugins_registered) do
    if not plugin_list[k] then
      plugin_list[k] = {}
    end
    plugin_list[k]["plugin_registered"] = v
  end

  -- 插件版本
  -- for plugin_name, _ in pairs(plugin_list) do
  --   local ok, res = pcall(require, "kong.plugins." .. plugin_name .. ".handler")
  --   if (not ok) or type(res) ~= "table" then
  --     plugin_list[plugin_name]["plugin_version"] = "handler.lua 不存在"
  --     plugin_list[plugin_name]["plugin_priority"] = "handler.lua 不存在"
  --   else
  --     plugin_list[plugin_name]["plugin_version"] = res.VERSION or res.version or "插件版本信息不存在"
  --     plugin_list[plugin_name]["plugin_priority"] = res.PRIORITY or res.priority or "插件优先级信息不存在"
  --   end
  -- end

  return plugin_list
end


--- 校验配置是否合理
local function check_plugins(conf_path, conf_name)
  local conf_path = conf_path
  local conf_name = conf_name

  -- 检测插件使用状况
  local plugin_list, err = plugin_list(conf_path, conf_name)
  if not plugin_list then
    local msg = "插件列表获取失败：" .. err
    return nil, msg
  end

  local check_error = {}
  local check_warning = {}

  for k, _ in pairs(plugin_list) do

    if plugin_list[k]["plugin_registered"] == "custom" and (not plugin_list[k]["plugin_file_existed"])
    then
      local msg = "自定义插件 ".. k .." 已注册但未找到插件文件。\n"
      table.insert(check_error, msg)
    end

    if plugin_list[k]["plugin_in_use"] and (not plugin_list[k]["plugin_registered"])
    then
      local msg = "自定义插件 ".. k .." 在使用中但未被注册。\n"
      table.insert(check_error, msg)
    end

    if plugin_list[k]["plugin_file_existed"] and (not plugin_list[k]["plugin_registered"])
    then
      local msg = "自定义插件 ".. k .." 文件存在但未被注册。\n"
      table.insert(check_warning, msg)
    end
  end

  if #check_error > 0 then
    return nil, "错误：\n", check_error
  end

  return true, "提示：\n", check_warning
end


--- 获取插件列表
local function get_plugin_list(conf_path, conf_name)
  local conf_path = conf_path
  local conf_name = conf_name
  local plugin_list, err = plugin_list(conf_path, conf_name)

  if not plugin_list then
    local msg = "插件列表获取失败：" .. err
    return nil, msg
  end

  local res = {}

  -- 拼接json
  for plugin_name, plugin_in_list in pairs(plugin_list) do
    local plugin = {}
    plugin.name = plugin_name

    if plugin_in_list["plugin_file_existed"] then
      plugin.plugin_file_existed = true
    else
      plugin.plugin_file_existed = false
    end

    if plugin_in_list["plugin_registered"] then
      plugin.plugin_registered = true
    else
      plugin.plugin_registered = false
    end

    if plugin_in_list["plugin_loaded"] then
      plugin.plugin_loaded = true
    else
      plugin.plugin_loaded = false
    end

    if plugin_in_list["plugin_in_use"] then
      plugin.plugin_in_use = true
    else
      plugin.plugin_in_use = false
    end

    local plugin_handler = kong.db.daos.plugins.handlers[plugin_name]

    local plugin_version
    local plugin_priority

    -- 获取插件版本和优先级
    if plugin_handler and type(plugin_handler) == "table" then
      plugin_version = plugin_handler.VERSION or "未知"
      plugin_priority = plugin_handler.PRIORITY or 0
    end

    plugin["plugin_version"]  = plugin_version or "未加载"
    plugin["plugin_priority"] = plugin_priority or 0

    res[plugin_name] = plugin
  end

  return true, res
end


--- 检测master节点是否存活
local function is_master_pod_alive(cluster_master)
  local sql = string.format(SELECT_MASTER_POD_ALIVE_SQL, cluster_master.master_pod_name)

  local db = kong.db
  local connector = db.connector
  local res, err = connector:query(sql)
  if not res or type(res) ~= "table" then
    local err = "查询master节点失败：" .. err
    kong.log.warn(err)
    return nil
  end

  if #res < 1 then
    local err = "查询master节点失败：集群不存在master节点"
    kong.log.warn(err)
    return nil

  elseif tonumber(res[1].interval_last_seen) > tools.get_update_seen_interval() + 1 then
    local err = "master节点失活：距master节点上次心跳已过" .. res[1].interval_last_seen .. "秒。"
    kong.log.warn(err)
    return nil

  else
    local msg = "master节点" .. cluster_master.master_pod_name .."存活：距master节点上次心跳已过" .. res[1].interval_last_seen .. "秒。"
    kong.log.debug(msg)
    return true
  end
end


--- 初始化节点信息
local function init_pod_info()
  local pod_config_version = tools.get_config_version() or "DEFAULT_VERSION"
  local pod_hostname = tools.get_pod_hostname()
  local pod_ip = tools.get_pod_ip()
  local pod_cluster = tools.get_pod_cluster()
  local pod_id, _ = kong_shared:get("pod_id")
  local ok, plugin_list = get_plugin_list(tools.get_default_conf_path(), tools.get_default_conf_name())
  if not ok then
    kong.log.warn("获取节点插件列表失败")
    plugin_list = {}
  end
  local pod_plugins = json.encode(plugin_list)
  local cluster_id = kong_shared:get("cluster_id")

  if not pod_id then
    pod_id = uuid()
    kong_shared:set("pod_id", pod_id)
  end

  if not cluster_id then
    cluster_id = tools.get_cluster_id()
    kong_shared:set("cluster_id", cluster_id)
  end

  local sql = string.format(INSERT_POD_INFO_SQL,
    pod_id, pod_hostname, pod_ip, pod_plugins, cluster_id, pod_cluster, pod_config_version,
    pod_ip, pod_plugins, cluster_id, pod_cluster, pod_config_version)

  -- kong.log.debug("新增节点信息SQL：" .. sql)

  local db = kong.db
  local connector = db.connector
  local res, err = connector:query(sql)
  if not res then
    local err = "更新节点信息失败：" .. err
    kong.log.warn(err)
    return nil, err
  end

  local msg = "写入节点信息：" .. pod_hostname

  -- kong.log.debug(msg)
  return true, msg
end


--- 更新节点
local function update_pod_info(item, data)
  if not item then
    return nil, "字段为空。\n"
  end
  if not data then
    return nil, "字段值为空。\n"
  end

  local pod_hostname = tools.get_pod_hostname()

  local sql = string.format(UPDATE_POD_INFO_SQL, item, data, pod_hostname)

  -- kong.log.warn("更新节点信息SQL：" .. sql)

  local db = kong.db
  local connector = db.connector
  local res, err = connector:query(sql)

  -- kong.log.debug("更新行数：" .. res.affected_rows)
  if res and res.affected_rows < 1 then
    init_pod_info()
  end

  if res then
    return true, "更新节点信息成功：" .. pod_hostname

  else
    local msg = "第一次尝试更新失败：" .. err
    res, err = connector:query(sql)

    if not res then
      msg = msg .. "第二次尝试更新失败：" .. err
      return nil, msg

    else
      msg = msg .. "第二次尝试更新成功。"
      return true, msg
    end
  end
end


--- 设置重启
local function set_pod_reload(pod_hostname)
  if not pod_hostname then
    return nil, "节点名为空。\n"
  end

  local sql = string.format(UPDATE_POD_INFO_SQL, "pod_last_reload", "NULL", pod_hostname)

  -- kong.log.warn("更新节点信息SQL：" .. sql)

  local db = kong.db
  local connector = db.connector
  local res, err = connector:query(sql)

  -- kong.log.debug("更新行数：" .. res.affected_rows)
  if res and res.affected_rows < 1 then
    init_pod_info()
  end

  if res then
    return true, "更新节点信息成功：" .. pod_hostname

  else
    local msg = "第一次尝试更新失败：" .. err
    res, err = connector:query(sql)

    if not res then
      msg = msg .. "第二次尝试更新失败：" .. err
      return nil, msg

    else
      msg = msg .. "第二次尝试更新成功。"
      return true, msg
    end
  end
end


--- 查询节点
local function get_pod_info(pod_hostname)
  local pod_hostname = pod_hostname

  local res = {}
  if pod_hostname then
    local field ={
      pod_hostname = pod_hostname
    }
    local select_res = kong.db.cluster_pods:select(field)
    if select_res then
      res[1] = select_res
    else
      return nil, "未找到主机：" .. pod_hostname
    end
  else
    res = kong.db.cluster_pods:page()

    if not res or #res == 0 then

      local update_seen_interval = tools.get_update_seen_interval()

      return nil, "未找到主机，请" .. update_seen_interval .. "秒后重试。"
    end
  end

  local timeZone = 0
  local pod_list = {}

  for _, pod in pairs(res) do
    pod.pod_start_time  = os.date("%Y-%m-%d %H:%M:%S",pod.pod_start_time + timeZone)
    pod.pod_last_seen   = os.date("%Y-%m-%d %H:%M:%S",pod.pod_last_seen + timeZone)
    if not pod.pod_last_reload then
      pod.pod_last_reload = "等待重载"
    else
      pod.pod_last_reload = os.date("%Y-%m-%d %H:%M:%S",pod.pod_last_reload + timeZone)
    end
    if not pod.pod_plugins then
      pod.pod_plugins = "{}"
    end
    pod.pod_plugins = json.decode(pod.pod_plugins)
    -- pod_list[pod.pod_hostname] = pod
    table.insert(pod_list, pod)
  end

  return true, pod_list
end


--- 新增插件
-- @param plugin_name 插件名
-- @param plugin_file 插件文件
local function add_plugin(plugins_path, plugin_name, plugin_file)
  local plugins_path = plugins_path
  local plugin_name = plugin_name
  local plugin_file = plugin_file

  -- 创建目录
  local ok, err  = os.execute("mkdir -p " .. plugins_path)
  if not ok then
    local msg = "建立目录失败：" .. err
    return nil, msg
  end

  if tools.file_exists(plugins_path .. plugin_name) then
    local res_msg = "推送的插件已存在。"
    local res_tag, msg = register_plugin(plugin_name, tools.get_default_conf_path(), tools.get_default_conf_name())

    res_msg = res_msg ..msg
    return res_tag, res_msg
  end

  local file, err = io.open(plugins_path .. plugin_name .. ".zip", "w+")

  if not file then
    local msg = "打开文件失败：" .. err
    return nil, msg
  end

  local ok, err = file:write(plugin_file)
  if not ok then
    local msg = "插件上传失败：" .. err
    file:close(file)
    return nil, msg
  end

  file:close(file)
  local res_msg = "插件上传成功。\n"

  -- 解压文件
  local res_tag, err = os.execute("unzip " .. plugins_path .. plugin_name .. ".zip -d " .. plugins_path .. plugin_name)
  if not res_tag then
    res_msg = res_msg .. "插件解压失败：".. err .. "\n"
  else
    res_msg = res_msg .. "插件解压成功。\n"
  end

  -- 清理压缩文件
  local ok, err = os.execute("rm " .. plugins_path .. plugin_name .. ".zip")
  if not ok then
    res_msg = res_msg .. "清理压缩文件失败：" .. err
  else
    res_msg = res_msg .. "清理压缩文件成功。\n"
  end

  -- 校验插件合法性
  local handler_file = plugins_path .. plugin_name .. "/handler.lua"
  local schema_file = plugins_path .. plugin_name .. "/schema.lua"

  if tools.file_exists(handler_file) and tools.file_exists(schema_file) then
    res_msg = res_msg .. "插件包包含 schema.lua 和 handler.lua ，校验通过。\n"

  else
    res_msg = res_msg .. "插件包不包含 schema.lua 和 handler.lua ，校验不通过，清理插件文件。\n"
    res_tag = nil
    -- 清理插件文件
    local ok, err = os.execute("rm -rf " .. plugins_path .. plugin_name)
    if not ok then
      res_msg = res_msg .. "清理插件文件失败：" .. err .. "\n"

    else
      res_msg = res_msg .. "清理插件文件成功。\n"
    end
  end

  if not res_tag then
    return res_tag, res_msg
  end

  local res_tag, msg = register_plugin(plugin_name, tools.get_default_conf_path(), tools.get_default_conf_name())
  res_msg = res_msg ..msg

  return res_tag, res_msg
end


--- 更新插件
-- @param plugin_name 插件名
-- @param plugin_file 插件文件
local function update_plugin(plugins_path, plugin_name, plugin_file)
  local plugins_path = plugins_path
  local plugin_name = plugin_name
  local plugin_file = plugin_file

  -- 创建目录
  local ok, err  = os.execute("mkdir -p " .. plugins_path)
  if not ok then
    local msg = "建立目录失败：" .. err
    return nil, msg
  end

  local current_timestamp = time() * 1000 -- 获取当前时间戳

  local new_plugin_name = "deleted_" .. plugin_name .. "_" .. current_timestamp --暂存文件名

  local plugin_exist_tag = tools.file_exists(plugins_path .. plugin_name) -- 插件是否存在

  local res_msg = ""
  -- 已有插件迁移
  if plugin_exist_tag then

    res_msg = "更新的插件已存在。\n"

    local ok, err  = os.execute("mv " .. plugins_path .. plugin_name .. " " .. plugins_path .. new_plugin_name)
    if not ok then
      res_msg = res_msg .. "暂存原插件文件失败：" .. err
      return nil, res_msg
    end

    res_msg = res_msg .. "暂存原插件文件成功。\n"
  end

  -- 保存新插件压缩包
  local file, err = io.open(plugins_path .. plugin_name .. ".zip", "w+")

  if not file then
    res_msg = res_msg .. "打开压缩文件失败：" .. err
    local _, msg = roll_back_move_plugin(plugin_exist_tag, plugins_path, plugin_name, new_plugin_name)
    res_msg = res_msg .. msg
    return  nil, res_msg
  end

  local res_tag, err = file:write(plugin_file)
  if not res_tag then
    res_msg = res_msg .. "插件压缩文件上传失败：" .. err
    file:close(file)
    local _, msg = roll_back_move_plugin(plugin_exist_tag, plugins_path, plugin_name, new_plugin_name)
    res_msg = res_msg .. msg
    return  nil, res_msg
  end

  file:close(file)
  res_msg = res_msg .. "插件上传成功。\n"

  -- 解压文件
  local res_tag, err = os.execute("unzip " .. plugins_path .. plugin_name .. ".zip -d " .. plugins_path .. plugin_name)

  if not res_tag then
    res_msg = res_msg .. "插件解压失败：".. err .. "\n"
    local _, msg = roll_back_move_plugin(plugin_exist_tag, plugins_path, plugin_name, new_plugin_name)
    res_msg = res_msg .. msg
  else
    res_msg = res_msg .. "插件解压成功。\n"
  end

  -- 校验插件合法性
  local handler_file = plugins_path .. plugin_name .. "/handler.lua"
  local schema_file = plugins_path .. plugin_name .. "/schema.lua"

  if tools.file_exists(handler_file) and tools.file_exists(schema_file) then
    res_msg = res_msg .. "插件包包含 schema.lua 和 handler.lua ，校验通过。\n"

  else
    res_msg = res_msg .. "插件包不包含 schema.lua 和 handler.lua ，校验不通过，清理插件文件。\n"
    res_tag = nil
    -- 回退
    local _, msg = roll_back_move_plugin(plugin_exist_tag, plugins_path, plugin_name, new_plugin_name)
    res_msg = res_msg .. msg
  end

  -- 清理压缩文件
  local ok, err = os.execute("rm " .. plugins_path .. plugin_name .. ".zip")
  if not ok then
    res_msg = res_msg .. "清理压缩文件失败：" .. err
  else
    res_msg = res_msg .. "清理压缩文件成功。\n"
  end

  if not res_tag then
    return res_tag, res_msg
  end

  local res_tag, msg = register_plugin(plugin_name, tools.get_default_conf_path(), tools.get_default_conf_name())
  res_msg = res_msg .. msg .. "\n"


  if plugin_exist_tag then

    local ok, err  = os.execute("rm -rf " .. plugins_path .. new_plugin_name)
    if not ok then
      res_msg = res_msg .. "删除原插件文件失败：" .. err .. "\n"
    end

    res_msg = res_msg .. "删除原插件成功。\n"
  end

  local res_tag, msg = register_plugin(plugin_name, tools.get_default_conf_path(), tools.get_default_conf_name())
  res_msg = res_msg ..msg

  return res_tag, res_msg
end


--- 删除插件
-- @param plugin_name 插件名
local function delete_plugin(plugins_path, plugin_name)
  local plugins_path = plugins_path
  local plugin_name = plugin_name

  local plugin_in_use = check_plugin_in_use(plugin_name)

  if #plugin_in_use > 0 then
    local msg = "插件删除失败，插件正作用于以下 " .. #plugin_in_use .. " 个位置：\n"
    for _, v  in pairs(plugin_in_use) do
      msg = msg .. v .. "\n"
    end
    return nil, msg
  end

  if not tools.file_exists(plugins_path .. plugin_name)  then
    local res_msg = "删除的插件文件不存在。\n"

    local res_tag, msg = unregister_plugin(plugin_name)
    res_msg = res_msg .. msg .. "\n"
    return res_tag, msg
  end

  -- 删除插件文件
  local res_tag, err, errcode = os.execute("rm -rf " .. plugins_path .. plugin_name)

  if not res_tag then
    local msg = "插件删除失败：" .. err .. "," .. tostring(errcode)
    return nil, msg
  end
  local res_msg = "插件删除成功。\n"

  local res_tag, msg = unregister_plugin(plugin_name)
  res_msg = res_msg .. msg .. "\n"

  return res_tag, res_msg
end


--- 清理ZIP包
local function zip_clean(plugins_path)
  local plugins_path = plugins_path

  -- 使用ls命令列出插件文件夹内zip
  local exe = io.popen("ls ".. plugins_path .. "*.zip |awk '{print $0}'")

  local succeeded = 0
  local failed = 0
  local res_tag = true
  local res_msg = ""

  for zip in exe:lines() do
    local ok, err = os.execute("rm " .. zip)
    if not ok then
      res_tag = false
      failed = failed + 1
      res_msg = res_msg .. "删除压缩文件：" .. zip .. " 失败:".. err .. "\n"
    else
      succeeded = succeeded + 1
      res_msg = res_msg .. "压缩文件：" .. zip .. " 删除成功。\n"
    end
  end

  exe:close()

  res_msg = res_msg .. string.format("压缩文件清理完成：成功%d/%d, 失败 %d。\n", succeeded, succeeded + failed, failed)
  return res_tag, res_msg
end


--- 校验引擎配置文件合法性
local function prepare_engine_config(conf_path, conf_name)
  local conf_path = conf_path
  local conf_name = conf_name

  local absolute_path = conf_path .. conf_name

  if not tools.file_exists(absolute_path) then
    local msg = "待检查的配置文件不存在"
    return nil, msg
  end

  -- 检测插件使用状况
  local ok, msg, detail = check_plugins(conf_path, conf_name)

  local res_msg = ""
  for _, v in pairs(detail) do
    res_msg = res_msg .. v
  end

  if not ok then
    local err = msg .. res_msg
    return nil, err
  end

  local res_msg = res_msg .. "插件校验通过。\n"

  local res = io.popen("/usr/local/bin/kong check " .. absolute_path.. " --vv")
  if res then
    res_msg = res_msg .. "kong 配置校验结果：\n"

    for row in res:lines() do
      res_msg = res_msg .. row .. "\n"
    end

    res:close()
    return true, res_msg
  end

  local res_msg = res_msg .. "kong 配置校验失败或配置校验结果为空。\n"

  return nil, res_msg
end


--- 重新加载引擎配置
local function reload_engine_config(conf_path, conf_name)
  local conf_path = conf_path
  local conf_name = conf_name

  local absolute_path = conf_path .. conf_name
  local success

  if not tools.file_exists(absolute_path) then
    local msg = "待重新加载配置文件不存在。"
    return success, msg
  end

  -- 检测插件使用状况
  local ok, msg, detail = check_plugins(conf_path, conf_name)

  local res_msg = ""

  if not ok then
    for _, v in pairs(detail) do
      res_msg = res_msg .. v
    end
    local err = msg .. res_msg
    return success, err
  end

  local res_msg = res_msg .. "插件校验通过。\n"
  local cmd = ""

  local env_list = tools.get_env_list()

  for k, v in pairs(env_list) do
    if string.find(k, [[^KONG]]) or string.find(k, [[^kong]]) then
      cmd = cmd .. string.format('export %s="%s"\n', k, v)
      cmd = cmd .. string.format('echo %s="%s"\n', k, v)
      kong.log.debug(k .. ":" .. v)
    end
  end

  cmd = cmd .. string.format(KONG_RELOAD_CMD, absolute_path, absolute_path)
  kong.log.debug(cmd)
  -- local res = io.popen("/usr/local/bin/kong reload -c " .. absolute_path)
  local worker_id = ngx.worker.id()
  local kong_pod_reload_shell_path = "/tmp/kong_pod_reload_shell_" .. worker_id .. ".sh"

  local file, err = io.open(kong_pod_reload_shell_path, "w+")

  if not file then
    local msg = "打开文件失败：" .. err
    kong.log.warn(msg)
    return nil, msg
  end

  local ok, err = file:write(cmd)
  if not ok then
    local msg = "文件写入失败：" .. err
    kong.log.warn(msg)
    file:close(file)
    return nil, msg
  end
  file:close(file)
  local ok = os.execute("chmod 777 " .. kong_pod_reload_shell_path)
  if not ok then
    local msg = "权限配置失败"
    kong.log.warn(msg)
    return nil, msg
  end

  local res = io.popen(kong_pod_reload_shell_path)

  if res then
    res_msg = res_msg .. "kong 重新加载结果：\n"
    local res_detail = ""
    for row in res:lines() do
      res_detail = res_detail .. row .. "\n"
      if string.find(row, "Kong reloaded") then
        success = true
      end
    end
    res:close()

    if success then
      res_msg = res_msg .. "Kong reloaded"
      local ok, err = update_pod_info("pod_last_reload", "now()")

      if not ok then
        res_msg = res_msg .. "更新节点状态失败：" .. err
      end

    else
      res_msg = res_msg .. "加载失败。" .. res_detail
    end

    return success, res_msg
  end

  local ok = os.execute("rm " .. kong_pod_reload_shell_path)
  if not ok then
    local msg = "脚本清理失败"
    kong.log.warn(msg)
  end

  local res_msg = res_msg .. "kong 重新加载失败或重新加载结果为空。\n"

  return success, res_msg
end


--- 线程中止
local function thread_kill(premature, thread)
  if premature then
    return
  end
  if not thread.thread then
    return
  end
  local ok, err= ngx.thread.kill(thread.thread)
  if not ok then
    kong.log.err("sdadas", err)
  end
end


--- 批量重载配置
local function batch_reload_pod(pods_hostname)
  local conf_path = tools.get_default_conf_path()
  local conf_name = tools.get_default_conf_name()

  -- 校验插件，失败报错
  local ok, msg, detail = check_plugins(conf_path, conf_name)
  if not ok then
    local res_msg = ""
    for _, v in pairs(detail) do
      res_msg = res_msg .. v
    end
    local err = msg .. res_msg
    return nil, err
  end

  local pods_hostname = pods_hostname
  local ok, pod_list = get_pod_info()

  if not ok then
    return nil, "节点列表为空：" .. pod_list
  end
  local res = {}

  -- 查找节点
  if pods_hostname then
    for _, pod_hostname in pairs(pods_hostname) do
      local pod_res = {}
      pod_res.found = false
      for _, pod in pairs(pod_list) do
        if pod.pod_hostname == pod_hostname then
          pod_res.found = true
          pod_res.result = false
          pod_res.message = ""
          pod_res.pod_hostname = pod.pod_hostname
          pod_res.pod_ip = pod.pod_ip
          pod_res.pod_cluster = pod.pod_cluster
        end
      end
      res[pod_hostname] = pod_res
    end
  else
    for _, pod in pairs(pod_list) do
      local pod_res = {}
      pod_res.found = true
      pod_res.result = false
      pod_res.message = ""
      pod_res.pod_hostname = pod.pod_hostname
      pod_res.pod_ip = pod.pod_ip
      pod_res.pod_cluster = pod.pod_cluster
      res[pod.pod_hostname] = pod_res
    end
  end

  local success = true
  local reload_res = {}

  if not res then
    success = false
    return nil, "未找到节点。"
  else
    local count = 0
    for _, v in pairs(res) do
      count = count + 1
      local ok, err = set_pod_reload(v.pod_hostname)
      if ok then
        v["result"] = true
        v["message"] = "kong reloading"
      else
        v["result"] = false
        v["message"] = "kong reload失败：" .. err or ""
      end
      table.insert(reload_res, v)
    end
    kong.log.debug("找到" .. count .. "个节点")
    if count == 0 then
      success = false
      return nil, "查找节点结果为空。"
    end
  end

  return success, reload_res
end


--- 批量重载配置(废弃)
local function batch_reload_pod_old(pods_hostname)
  local pods_hostname = pods_hostname
  local ok, pod_list = get_pod_info()
  local pod_cluster = tools.get_pod_cluster()

  if not ok then
    return nil, "节点列表为空：" .. pod_list
  end
  local res = {}

  -- 查找节点
  if pods_hostname then
    for _, pod_hostname in pairs(pods_hostname) do
      local pod_res = {}
      pod_res.found = false
      for _, pod in pairs(pod_list) do
        if pod.pod_hostname == pod_hostname then
          pod_res.found = true
          pod_res.pod_ip = pod.pod_ip
          pod_res.pod_cluster = pod.pod_cluster
        end
      end
      res[pod_hostname] = pod_res
    end
  else
    for _, pod in pairs(pod_list) do
      local pod_res = {}
      pod_res.found = true
      pod_res.pod_hostname = pod.pod_hostname
      pod_res.pod_ip = pod.pod_ip
      pod_res.pod_cluster = pod.pod_cluster
      res[pod.pod_hostname] = pod_res
    end
  end

  local pod_to_reload = {}
  if not res then
    return nil, "未找到节点。"
  else
    local count = 0
    for _, v in pairs(res) do
      count = count + 1
      if v.found then
        if not pod_to_reload[v.pod_cluster] then
          pod_to_reload[v.pod_cluster] = {}
        end
        table.insert(pod_to_reload[v.pod_cluster], v)
      end
    end
    kong.log.debug("找到" .. count .. "个节点")
    if count == 0 then
      return nil, "查找节点结果为空。"
    end
  end

  local success = true
  local reload_res = {}

  local pods_in_this_cluster = pod_to_reload[pod_cluster]
  pod_to_reload[pod_cluster] = nil

  local threads = {}

  if pod_cluster == "admin" then
    -- 循环重新加载其他实例节点
    local clusters = tools.get_cluster_list()
    for cluster, pods in pairs(pod_to_reload) do
      if not clusters[cluster] or not clusters[cluster].cluster_url then
        for _, pod in pods do
          pod.result = false
          pod.message = "节点实例未找到。"
          success = false
          table.insert(reload_res, pod)
        end

      else
        local http_endpoint = "http://" .. clusters[cluster].cluster_url .. "/plugin_manage/batch_reload_pod"
        --local http_endpoint = "http://127.0.0.1:11081/plugin_manage/batch_reload_pod"

        local pods_hostname = {}

        for _, pod in pairs(pods) do
          table.insert(pods_hostname, pod.pod_hostname)
        end

        local pods_string = json.encode(pods_hostname)

        -- 新建线程发送请求
        local thread = ngx.thread.spawn(http_sender.send_http_request, http_endpoint, "GET", pods_string)

        -- 初次失败则重试
        if not thread then
          kong.log.warn("插件管理：新建线程失败，二次尝试。")
          thread = ngx.thread.spawn(http_sender.send_http_request, http_endpoint, "GET", pods_string)
        end

        -- 二次失败记录错误日志
        if not thread then
          kong.log.err("插件管理：新建线程失败，二次尝试失败。")
        else
          -- 新建线程成功，放入列表
          local thread_and_pods_info = {
            thread = thread,
            pods = pods,
          }
          table.insert(threads, thread_and_pods_info)
        end
      end
    end

    -- 等待每一个线程
    for _, element in pairs(threads) do
      local ok, msg = ngx.thread.wait(element.thread)
      if not ok then
        kong.log.err("部分集群重新加载配置失败。")
      else
        if not ok then
          success = false
        end
        if msg and type(msg) == "table" then
          for _, pod in pairs(msg) do
            table.insert(reload_res, pod)
          end
        else
          for _, pod in pairs(element.pods) do
            pod.result = false
            pod.message = "请求发送失败，请检查节点实例网络连通性:" .. msg or "null"
            table.insert(reload_res, pod)
          end
        end
      end
    end
  end

  -- 循环重新加载本实例节点
  if pods_in_this_cluster then

    local threads = {}

    for _, pod in pairs(pods_in_this_cluster) do
      -- pod.pod_hostname = pod_hostname
      if pod.found == false then
        pod.result = false
        success = false
        table.insert(reload_res, pod)
      else
        local ip = pod.pod_ip
        local http_endpoint = "http://" .. ip .. ":8001/plugin_manage/reload_engine_config"
        local thread = ngx.thread.spawn(http_sender.send_http_request, http_endpoint, "GET", "")

        -- 初次失败则重试
        if not thread then
          kong.log.warn("插件管理：新建线程失败，二次尝试。")
          thread = ngx.thread.spawn(http_sender.send_http_request, http_endpoint, "GET", "")
        end

        -- 二次失败记录错误日志
        if not thread then
          kong.log.err("插件管理：新建线程失败，二次尝试失败。")
        else
          -- 新建线程成功，放入列表
          local thread_and_pods_info = {
            thread = thread,
            pod = pod,
          }
          table.insert(threads, thread_and_pods_info)
        end
      end
    end

    -- 等待每一个线程
    for _, element in pairs(threads) do
      local ok, msg = ngx.thread.wait(element.thread)
      if not ok then
        element.pod.result = false
        element.pod.message = msg
        success = false
      else
        element.pod.result = true
        element.pod.message = msg
      end
      table.insert(reload_res, element.pod)
    end
  end

  return success, reload_res
end


--- 循环任务
local function cycle_task(premature)
  if premature then -- 若因worker退出被触发，直接返回
    return
  end

  local pod_hostname = tools.get_pod_hostname()
  local pod_ip = tools.get_pod_ip()
  local pod_cluster = tools.get_pod_cluster()

  local ok, msg = update_pod_info("pod_last_seen", "now()")
  if not ok then
    kong.log.warn(msg)

  else
    kong.log.debug(msg)
  end

  -- 获取插件串
  local ok, plugin_list = get_plugin_list(tools.get_default_conf_path(), tools.get_default_conf_name())
  if not ok then
    kong.log.warn("获取节点插件列表失败")
    plugin_list = {}
  end
  local pod_plugins = json.encode(plugin_list)

  -- local pod_plugins = json.encode(plugin_list(tools.get_default_conf_path(), tools.get_default_conf_name()))
  local ok, msg = update_pod_info("pod_plugins", "'" .. string.gsub(pod_plugins, "'", "\\'") .. "'")
  if not ok then
    kong.log.warn(msg)

  else
    kong.log.debug(msg)
  end

  -- 内存、数据库获取master节点
  local cluster_master = tools.get_master_pod(pod_cluster)

  -- 判断当前节点是否为master节点
  if cluster_master and cluster_master.master_pod_name == pod_hostname then
    kong_shared:set("this_pod_is_master", true)

  else
    kong_shared:set("this_pod_is_master", false)
  end

  -- 判断master节点是否存活，失活则抢主
  if not cluster_master or not is_master_pod_alive(cluster_master) then
    local this_pod_is_master = tools.become_master_pod(pod_cluster, pod_hostname, pod_ip)

    if this_pod_is_master then -- 当前节点是master节点则记录
      kong_shared:set("this_pod_is_master", true)

    else -- 当前节点不是master节点则置为false
      kong_shared:set("this_pod_is_master", false)
    end
  end

  -- master节点清理过期节点
  if kong_shared:get("this_pod_is_master") then
    local ok, msg = tools.clean_up_expired_nodes()
    if not ok then
      kong.log.warn(msg)

    else
      kong.log.debug(msg)
    end
  end

  local field ={
    pod_hostname = pod_hostname
  }
  local res = kong.db.cluster_pods:select(field)

  -- 判断是否需要重载
  -- 判断master节点是否存活，失活则抢主
  if res and (not res.pod_last_reload or res.pod_last_reload == "") then
    local conf_path = tools.get_default_conf_path()
    local conf_name = tools.get_default_conf_name()
    reload_engine_config(conf_path, conf_name)
  end
end


--- 启动循环任务
local function start_cycle_task()
  if 1 == ngx.worker.id() then
    -- ngx_shared.pod_reload_time = ngx.time()

    local ok, err = init_pod_info()
    if not ok then
      kong.log.err("网关节点注册失败：", err)
      return nil
    end

    --从环境变量获取节点更新频率，获取失败则使用默认值
    local update_seen_interval = tools.get_update_seen_interval()

    local ok, err = ngx.timer.every(update_seen_interval, cycle_task)
    if not ok then
      kong.log.err("网关节点信息更新服务启动失败：", err)
      return nil
    end
    kong.log.debug("网关节点信息更新服务启动成功。")
  else

    kong.log.debug("非worker1.")
  end
  return true
end

--[[

local function file_up(plugin_name, plugin_file)
  local plugin_name = plugin_name
  local plugin_file = plugin_file

  local file, err = io.open("/nas/kong/kong/files/" .. plugin_name .. ".zip", "w+")

  if not file then
    local msg = "打开文件失败：" .. err
    return nil, msg
  end

  local ok, err = file:write(plugin_file)
  if not ok then
    local msg = "插件上传失败：" .. err
    file:close(file)
    return nil, msg
  end

  file:close(file)
  local res_msg = "插件上传成功。\n"

  return true, res_msg
end


local function file_down(plugin_path)
end
]]


return {
  DEFAULT_PLUGINS_PATH = tools.DEFAULT_PLUGINS_PATH,
  DEFAULT_CONF_PATH = tools.DEFAULT_CONF_PATH,
  DEFAULT_CONF_NAME = tools.DEFAULT_CONF_NAME,

  get_pod_hostname = tools.get_pod_hostname,
  get_pod_ip = tools.get_pod_ip,
  get_default_plugins_path = tools.get_default_plugins_path,
  get_default_conf_path = tools.get_default_conf_path,
  get_default_conf_name = tools.get_default_conf_name,

  HTTP_BAD_REQUEST = HTTP_BAD_REQUEST,
  HTTP_OK = HTTP_OK,

  register_plugin_exporter = register_plugin,
  unregister_plugin_exporter = unregister_plugin,
  get_plugin_list = get_plugin_list,
  add_plugin = add_plugin,
  update_plugin = update_plugin,
  delete_plugin = delete_plugin,
  zip_clean = zip_clean,
  prepare_engine_config = prepare_engine_config,
  reload_engine_config = reload_engine_config,
  start_cycle_task = start_cycle_task,
  get_pod_info = get_pod_info,
  batch_reload_pod = batch_reload_pod,

  -- test_cmd = test_cmd,
  -- file_up = file_up,
  -- file_down = file_down,
}
